#include "IcmpHelper.h"
#include <cstdint>
#include "PlatLogger.h"
#if defined(__CYGWIN__)
#else
#    include <cstdio>
#    include <netdb.h>
#    include <netinet/icmp6.h>
#    include <netinet/in_systm.h>
#    include <netinet/ip.h>
#    include <netinet/ip_icmp.h>
#    include <sys/socket.h>
#    include <sys/time.h>
#    include <thread>
#    include <unistd.h>
#    include <utility>
#    include "IpV4Address.h"
#    include "IpV6Address.h"
#    include "SocketOpt.h"
#    include "XnccAddress.h"

#    include "Util.h"
namespace xncc {
namespace foundation {

    IcmpHelper::IcmpHelper(std::shared_ptr<XnccAddress> addr)
        : addr_(std::move(addr)), pid_(getpid() & 0xffff), nsent_(0)
    {
        const std::string addrstr = addr_->toStr();
        if (addr_->Type() == shared::XNCC_ADDR_TYPE::IPV4_TCP || addr_->Type() == shared::XNCC_ADDR_TYPE::IPV4_UDP) {
            icmpProtocol_ = IPPROTO_ICMP;
        }
        else if (addr_->Type() == shared::XNCC_ADDR_TYPE::IPV6_TCP ||
                 addr_->Type() == shared::XNCC_ADDR_TYPE::IPV6_UDP) {
            icmpProtocol_               = IPPROTO_ICMPV6;
            const std::string addstr    = addr_->toStr();
            const std::string ipaddrstr = addstr.substr(0, addrstr.find_last_of(":"));
            ar_                         = hostServ(ipaddrstr.c_str(), nullptr, 0, 0);
        }
        fd_            = SocketOpt::createOrDie(addr_->family(), icmpProtocol_, SOCK_RAW);
        const int size = 60 * 1024;
        setsockopt(fd_, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));
    }

    void IcmpHelper::ping()
    {
        if (icmpProtocol_ == IPPROTO_ICMP) {
            sendV4();
        }
        else {
            initV6();
            sendV6();
        }
        recvMsg();
    }

    void IcmpHelper::initV6()
    {
        int on = 1;
        /* install a filter that only passes ICMP6_ECHO_REPLY  */
        struct icmp6_filter myfilt;
        ICMP6_FILTER_SETBLOCKALL(&myfilt);
        ICMP6_FILTER_SETPASS(ICMP6_ECHO_REPLY, &myfilt);
        setsockopt(fd_, IPPROTO_IPV6, ICMP6_FILTER, &myfilt, sizeof(myfilt));
        /* ignore error return; the filter is an optimization */

        /* ignore error returned below; we just won't receive the hop limit */
#    ifdef IPV6_RECVHOPLIMIT
        /* RFC 3542 */
        setsockopt(fd_, IPPROTO_IPV6, IPV6_RECVHOPLIMIT, &on, sizeof(on));
#    else
        /* RFC 2292 */
        setsockopt(sockfd, IPPROTO_IPV6, IPV6_HOPLIMIT, &on, sizeof(on));
#    endif
    }

    void IcmpHelper::recvMsg()
    {
        char recvbuf[BUFSIZE]    = {0};
        char controlbuf[BUFSIZE] = {0};

        struct sockaddr_in6 peerAddr;
        struct sockaddr*    peerSockaddr = reinterpret_cast<struct sockaddr*>(&peerAddr);
        unsigned int        sockLen      = sizeof(struct sockaddr);

        struct msghdr msg;
        struct iovec  iov;
        iov.iov_base = recvbuf;
        iov.iov_len  = sizeof(recvbuf);

        msg.msg_name    = peerSockaddr;
        msg.msg_iov     = &iov;
        msg.msg_iovlen  = 1;
        msg.msg_control = controlbuf;
        for (;;) {
            msg.msg_namelen    = sockLen;
            msg.msg_controllen = sizeof(controlbuf);
            ssize_t n          = ::recvmsg(fd_, &msg, 0);
            if (n < 0) {
                if (errno == EINTR)
                    continue;
                else
                    SYSTEM_ERROR << "recvmsg error"_s;
            }
            if (peerSockaddr->sa_family == AF_INET) {
                auto v4PAdr = std::make_shared<IpV4Address>(IPPROTO_UDP);
                v4PAdr->setSockAddr(*SocketOpt::sockaddr_in_cast(peerSockaddr));
                rspAddr_ = v4PAdr;
            }
            else if (peerSockaddr->sa_family == AF_INET6) {
                auto v6PAdr = std::make_shared<IpV6Address>(IPPROTO_UDP);
                v6PAdr->setSockAddr(*SocketOpt::sockaddr_in6_cast(peerSockaddr));
                rspAddr_ = v6PAdr;
            }
            struct timeval tval;
            gettimeofday(&tval, NULL);
            if (icmpProtocol_ == IPPROTO_ICMP) {
                procV4(recvbuf, static_cast<uint64_t>(n), &tval);
            }
            else {
                procV6(recvbuf, static_cast<uint64_t>(n), &msg, &tval);
            }
            break;
        }
    }

    void IcmpHelper::procV6(char* ptr, uint64_t len, struct msghdr* msg, struct timeval* tvrecv)
    {
        struct cmsghdr*   cmsg;
        int               hlim;
        struct icmp6_hdr* icmp6 = reinterpret_cast<struct icmp6_hdr*>(ptr);
        if (len < 8)
            return; /* malformed packet */

        if (icmp6->icmp6_type == ICMP6_ECHO_REPLY) {
            if (icmp6->icmp6_id != pid_)
                return; /* not a response to our ECHO_REQUEST */
            if (len < 16)
                return; /* not enough data to use */

            struct timeval* tvsend = reinterpret_cast<struct timeval*>(icmp6 + 1);
            tv_sub(tvrecv, tvsend);
            double rtt = static_cast<double>(tvrecv->tv_sec) * 1000.0 + static_cast<double>(tvrecv->tv_usec) / 1000.0;

            hlim = -1;
            for (cmsg = CMSG_FIRSTHDR(msg); cmsg != NULL; cmsg = CMSG_NXTHDR(msg, cmsg)) {
                if (cmsg->cmsg_level == IPPROTO_IPV6 && cmsg->cmsg_type == IPV6_HOPLIMIT) {
                    hlim = *reinterpret_cast<int*>(CMSG_DATA(cmsg));
                    break;
                }
            }
            printf("%lu bytes from %s: seq=%u, hlim=", len, rspAddr_->toStr().c_str(), icmp6->icmp6_seq);
            if (hlim == -1)
                printf("???"); /* ancillary data missing */
            else
                printf("%d", hlim);
            printf(", rtt=%.3f ms\n", rtt);
        }
        else {
            printf("%lu bytes from %s: type = %d, code = %d\n", len, rspAddr_->toStr().c_str(), icmp6->icmp6_type,
                   icmp6->icmp6_code);
        }
    }
    void IcmpHelper::procV4(char* ptr, uint64_t len, struct timeval* tvrecv)
    {
        struct ip* ip    = reinterpret_cast<struct ip*>(ptr); /* start of IP header */
        int        hlen1 = ip->ip_hl << 2;                    /* length of IP header */
        if (ip->ip_p != IPPROTO_ICMP)
            return; /* not ICMP */

        struct icmp* icmp    = reinterpret_cast<struct icmp*>(ptr + hlen1); /* start of ICMP header */
        int          icmplen = static_cast<int>(len) - hlen1;
        if (icmplen < 8)
            return; /* malformed packet */

        if (icmp->icmp_type == ICMP_ECHOREPLY) {
            if (icmp->icmp_id != pid_)
                return; /* not a response to our ECHO_REQUEST */
            if (icmplen < 16)
                return; /* not enough data to use */

            struct timeval* tvsend = reinterpret_cast<struct timeval*>(icmp->icmp_data);
            tv_sub(tvrecv, tvsend);
            double rtt = static_cast<double>(tvrecv->tv_sec) * 1000.0 + static_cast<double>(tvrecv->tv_usec) / 1000.0;

            printf("%d bytes from %s: seq=%u, ttl=%d, rtt=%.3f ms\n", icmplen, rspAddr_->toStr().c_str(),
                   icmp->icmp_seq, ip->ip_ttl, rtt);
        }
        else {
            printf("  %d bytes from %s: type = %d, code = %d\n", icmplen, rspAddr_->toStr().c_str(), icmp->icmp_type,
                   icmp->icmp_code);
        }
    }

    void IcmpHelper::tv_sub(struct timeval* out, struct timeval* in)
    {
        if ((out->tv_usec -= in->tv_usec) < 0) { /* out -= in */
            --out->tv_sec;
            out->tv_usec += 1000000;
        }
        out->tv_sec -= in->tv_sec;
    }
    void IcmpHelper::sendV6()
    {
        const size_t      datalen          = 56;
        char              sendbuf[BUFSIZE] = {0};
        struct icmp6_hdr* icmp6            = reinterpret_cast<struct icmp6_hdr*>(sendbuf);
        icmp6->icmp6_type                  = ICMP6_ECHO_REQUEST;
        icmp6->icmp6_code                  = 0;
        icmp6->icmp6_id                    = static_cast<uint16_t>(pid_);
        icmp6->icmp6_seq                   = nsent_++;
        memset((icmp6 + 1), 0xa5, datalen); /* fill with pattern */
        struct timeval* tv = reinterpret_cast<struct timeval*>(icmp6 + 1);
        gettimeofday(tv, NULL);
        const size_t len = 8 + datalen; /* 8-byte ICMPv6 header */
        auto         n   = sendto(fd_, sendbuf, len, 0, ar_->ai_addr, ar_->ai_addrlen);
        if (n != static_cast<ssize_t>(len)) {
            SYSTEM_ERROR << "sendto" << addr_->toStr() << " ret = " << n << "  socklen:" << addr_->getSockLen()
                         << "  error family: " << addr_->family();
        }
    }
    void IcmpHelper::sendV4()
    {
        const size_t datalen          = 56;
        char         sendbuf[BUFSIZE] = {0};
        struct icmp* icmp             = reinterpret_cast<struct icmp*>(sendbuf);
        icmp->icmp_type               = ICMP_ECHO;
        icmp->icmp_code               = 0;
        icmp->icmp_id                 = static_cast<uint16_t>(pid_);
        icmp->icmp_seq                = nsent_++;
        memset(icmp->icmp_data, 0xa5, datalen); /* fill with pattern */
        struct timeval* tv = reinterpret_cast<struct timeval*>(icmp->icmp_data);
        gettimeofday(tv, NULL);
        const size_t len = 8 + datalen; /* 8-byte ICMPv6 header */

        icmp->icmp_cksum = 0;
        icmp->icmp_cksum = in_cksum(reinterpret_cast<uint16_t*>(icmp), len);

        if (sendto(fd_, sendbuf, len, 0, addr_->getSockAddr(), addr_->getSockLen()) != static_cast<ssize_t>(len)) {
            SYSTEM_ERROR << ("sendto error"_s);
        }
    }
    uint16_t IcmpHelper::in_cksum(uint16_t* addr, int len)
    {
        int       nleft  = len;
        uint32_t  sum    = 0;
        uint16_t* w      = addr;
        uint16_t  answer = 0;

        /*
         * Our algorithm is simple, using a 32 bit accumulator (sum), we add
         * sequential 16 bit words to it, and at the end, fold back all the
         * carry bits from the top 16 bits into the lower 16 bits.
         */
        while (nleft > 1) {
            sum += *w++;
            nleft -= 2;
        }

        /* 4mop up an odd byte, if necessary */
        if (nleft == 1) {
            *reinterpret_cast<unsigned char*>(&answer) = *reinterpret_cast<unsigned char*>(w);
            sum += answer;
        }

        /* 4add back carry outs from top 16 bits to low 16 bits */
        sum = (sum >> 16) + (sum & 0xffff);   /* add hi 16 to low 16 */
        sum += (sum >> 16);                   /* add carry */
        answer = static_cast<uint16_t>(~sum); /* truncate to 16 bits */
        return (answer);
    }

    struct addrinfo* IcmpHelper::hostServ(const char* host, const char* serv, int family, int socktype)
    {
        int             n;
        struct addrinfo hints, *res;

        shared::memZero(hints);
        hints.ai_flags    = AI_CANONNAME; /* always return canonical name */
        hints.ai_family   = family;       /* 0, AF_INET, AF_INET6, etc. */
        hints.ai_socktype = socktype;     /* 0, SOCK_STREAM, SOCK_DGRAM, etc. */

        if ((n = getaddrinfo(host, serv, &hints, &res)) != 0)
            printf("host_serv error for %s, %s: %s", (host == NULL) ? "(no hostname)" : host,
                   (serv == NULL) ? "(no service name)" : serv, gai_strerror(n));

        return (res); /* return pointer to first on linked list */
    }

}  // namespace foundation
}  // namespace xncc
#endif